Cloudflare Caching Demo — Cheat Sheet

~15–20 min · Show caching going from DYNAMIC → HIT live · End at the dashboard analytics

Pre-demo setup checklist

1. Open (~1 min)

"Caching is the part of Cloudflare that already works for almost every customer — they just don't know how much. Static assets like images, CSS, JavaScript get cached automatically at 330+ data centers globally. What most customers miss is that HTML pages, API responses, and dynamic content can be cached too — they just need a rule."

"I'm going to show you a page that's NOT being cached right now, turn on caching in two clicks, and you'll see the difference in the dashboard immediately."

2. The Before State — HTML Going to Origin (~2 min)

BEFORE — every request goes to origin

▶ Switch to terminal

curl -sI https://nginx.tarheel.us/ | grep -iE "cf-cache|cf-ray|age"
"Output should show: cf-cache-status: DYNAMIC"

"DYNAMIC means Cloudflare didn't even try to cache this. Why? Because the URL doesn't have a file extension. Cloudflare's default cache keys off extensions — .png, .css, .js, .jpg. A URL like / or /about has no extension, so Cloudflare assumes it could be personalized — login pages, dashboards, anything dynamic — and sends every request straight to origin."

"Safe but slow. Every visitor to that homepage is hitting my origin server, even though the page hasn't changed in days."

▶ Run it 3 times in a row to drive the point home:

for i in 1 2 3; do curl -sI https://nginx.tarheel.us/ | grep cf-cache; done
"Three requests. Three DYNAMICs. Three round trips to my origin. Now let's fix it."

3. Add a Cache Rule (~3 min)

CONFIGURE — turn on HTML caching with one rule

▶ Dashboard: tarheel.us → Caching → Cache Rules → Create rule

Fill in:

▶ Click Deploy

"That's the entire change. One expression, one TTL, deployed globally in seconds. Cloudflare just pushed this rule to all 330+ data centers."

"Notice what we didn't have to do — touch the origin, modify any code, change any HTTP headers. The Cache Rule lives at the edge and overrides whatever defaults were in place."

4. The After State — Watch It Cache (~3 min)

AFTER — first request MISS, every request after is HIT

▶ Back to terminal

for i in 1 2 3 4; do curl -sI https://nginx.tarheel.us/ | grep cf-cache; sleep 1; done
"Watch the output:"
cf-cache-status: MISS    ← first request, Cloudflare fetched from origin and stored it
cf-cache-status: HIT     ← served from the edge
cf-cache-status: HIT     ← served from the edge
cf-cache-status: HIT     ← served from the edge
"There it is. One MISS to populate the cache, then every request after is a HIT served from the data center closest to me. The origin is no longer involved in 75% of these requests."

"In a real scenario with thousands of visitors, the ratio is even more dramatic — maybe one MISS for every thousand HITs."

▶ For an even better visual, run a verbose curl to show the age header growing:

curl -sI https://nginx.tarheel.us/ | grep -iE "cf-cache|age:"
"The age header tells you how many seconds ago Cloudflare cached it. Hit refresh a few times and watch it climb. That's edge time, not origin time."

5. Show It in the Dashboard (~3 min)

▶ Dashboard: tarheel.us → Analytics & Logs → Traffic (or Caching → Overview)

"Now let's pull up the analytics. This is where you can see caching working at scale — across every request, not just the ones we just ran."

What to point at:

"If you're a CFO looking at this page, what you should be asking is: 'how much smaller could my origin server be if Cloudflare absorbed even more of this?' That's the conversation."
"And remember — we made one change. One rule. No code deployed, no servers restarted, no DNS modified."
THE ARCHITECTURAL POINT

Static assets → cached automatically. No config needed.
HTML / dynamic content → one Cache Rule away.
Edge cache → 330+ locations. Fastest path to every visitor.
Origin load → drops by the size of your cache hit ratio.

"Most customers are already paying for Cloudflare. The bandwidth, the data centers, the cache — it's already there. The question is whether you're using it."

6. If Customer Asks — cf-cache-status Decoder

ValueWhat it means
HITServed from Cloudflare cache. The good outcome.
MISSNot in cache. Cloudflare fetched from origin and (usually) stored it.
BYPASSSomething told Cloudflare not to cache — usually a cookie, header, or rule.
DYNAMICCloudflare didn't try to cache. Default for URLs without static file extensions.
EXPIREDWas cached, but TTL ran out. Will refresh from origin.
REVALIDATEDWas cached, TTL expired, origin confirmed it's still fresh (304).
UPDATINGStale-while-revalidate: served from cache while async refresh happens.
STALEOrigin is down — served the stale cache anyway. Lifesaver.
NONE/UnknownCloudflare didn't cache (usually a non-GET method or zero-TTL).

You'll NOT see cf-cache-status on 3xx redirects, WAF blocks, Cloudflare error pages, or Worker fetch requests.


Q&A — back-pocket answers

What products fit which use case?

Static web content (images, CSS, JS, fonts, video) — Default cache, already on.

HTML pages, WordPress — APO (~$5/mo). Auto-purge on publish.

HTML pages, static site (Jekyll/Hugo/Astro) — Cache Rule, long Edge TTL, purge on deploy.

HTML pages, dynamic with logged-in users — Cache Rule that bypasses on auth cookie.

API responses, public — Cache Rules. If logic is complex, Workers + Cache API.

API responses, per-user — Workers KV. Cache the data, not the response.

Large files accessed frequently — Default cache + Tiered Cache.

Large files accessed rarely — Cache Reserve.

Object storage — R2. Zero egress fees.

App state inside a Worker — KV (eventually consistent) or Durable Objects (strongly consistent).

"My cache hit rate is low" — how do I diagnose?
  1. Check cf-cache-status. MISS/BYPASS/DYNAMIC point to different causes.
  2. Check origin Cache-Control headers. private and no-store prevent CDN caching.
  3. Check query strings — high cardinality fragments the cache.
  4. Check cookies — origin setting cookies can cause BYPASS.
  5. Check Vary headers — fragments cache by header value.
curl -svo /dev/null https://example.com/asset.png 2>&1 | \
  egrep -i '< cf-cache|< cache-control|< age|< vary|< set-cookie'
"My HTML isn't being cached"

HTML is NOT cached by default — only static assets by file extension. To cache HTML, create a Cache Rule:

If logged-in users see cached pages, add a bypass on auth cookie: AND cookie does not contain session_id

"My origin is being hammered"
  1. Turn on Tiered Cache (free, one toggle). Routes cache misses through upper-tier DCs first — fewer origin requests.
  2. Enable Cache Reserve. Persistent storage for content that gets evicted. Requires TTL ≥ 10 hours + Content-Length header.
  3. Audit Cache-Control headers — make sure origin isn't accidentally killing caching.
"I need to purge cache after a deploy"

Ranked by precision (most surgical first):

All available via API for CI/CD integration.

"My cache-control header is being ignored"
"Why is private content being cached?" / "How do I keep private content out of cache?"
Header precedence (which directive wins?)
  1. Cloudflare-CDN-Cache-Control (Cloudflare only)
  2. CDN-Cache-Control (any CDN)
  3. Cache-Control: s-maxage (shared caches)
  4. Cache-Control: max-age (any cache)
  5. Expires header (legacy, lowest priority)

If origin sends no directives at all, Cloudflare caches based on HTTP status code defaults.

"I want different TTLs for browser vs. Cloudflare"

Two ways:

  1. Origin sends both max-age (browser) and s-maxage (CDN). Example: max-age=0, s-maxage=86400.
  2. Cache Rules → set Edge Cache TTL and Browser Cache TTL independently.
"I want to cache differently per language / device / country"

Cache Rules → Custom Cache Key. Include in cache key:

Be careful with cardinality — too many variations fragment the cache.

Tiered Cache vs. Cache Reserve — which when?

Tiered Cache — free. Reduces requests to origin by having edge DCs check upper-tier DCs first. Recommend this first when origin is getting hammered.

Cache Reserve — paid (usage-based on storage + operations). Persistent storage that holds content even after it would normally be evicted. Recommend for:

Requires TTL ≥ 10 hours + Content-Length header. Tiered Cache strongly recommended alongside.

Workers + Cache API — when do I need it?

Use when Cache Rules aren't expressive enough. From inside a Worker you can:

Three methods: cache.match(), cache.put(), cache.delete().

When to push back on a customer ask

"Cache everything for 24 hours" — will serve stale logged-in pages and expose private info. Ask what changes when.

"Cache POST/PUT/DELETE requests" — those are mutations. Cache GET only.

"Cache by IP for personalization" — IPs are shared via CGNAT, proxies, NAT. Use cookies, headers, or Workers + KV.

"Should I use Page Rules or Cache Rules?" — Cache Rules. Page Rules are legacy.

"I want max-age=0 to mean don't cache" — max-age=0 means cached but immediately stale. Use no-store if you mean don't cache.

"I'll just bypass cache for everything" — defeats the point. Most pages have cacheable parts.

Pricing gates — quick reference